/**
 * The MIT License
 *
 * Copyright (c) 2010 Benjamin Klum
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.helgoboss.caching_obr_ref_impl

import org.osgi.service.obr._
import org.apache.felix.bundlerepository.impl.RepositoryAdminImpl
import org.apache.felix.bundlerepository.impl.wrapper.Wrapper
import org.apache.felix.utils.log.Logger 
import java.net.{URL, URLConnection}
import org.eclipse.scalamodules._
import org.osgi.framework.{BundleActivator, BundleContext, ServiceRegistration, Constants}
import org.helgoboss.caching_obr.CachingRepositoryAdmin
import java.security.MessageDigest
import java.math.BigInteger
import java.io.{File, FileOutputStream}
import org.apache.felix.bundlerepository.impl.{Base64Encoder, FileUtil, DataModelHelperImpl}
import org.helgoboss.scala_logging_osgi.OsgiLogging
import org.osgi.service.prefs.PreferencesService


object CachingRepositoryAdminRefImpl {
    private val ServiceRanking = 1
    private val PathToRepositoriesNode = "org/helgoboss/obr-repository-persistence/repositories"
}

class CachingRepositoryAdminRefImpl extends CachingRepositoryAdmin with BundleActivator with OsgiLogging {
    import CachingRepositoryAdminRefImpl._
    
    private var originalAdmin: RepositoryAdmin = _
    private var adminImpl: RepositoryAdminImpl = _
    protected var bundleContext: BundleContext = _
    private val surfaceRepositories = new collection.mutable.LinkedHashMap[URL, Repository]
    
    def addRepository(repositoryUrl: URL) = {
        addRepository(repositoryUrl, updateProgress = None)
    }
    
    private lazy val repositoryCacheDir = bundleContext.getDataFile("repository-cache")
    
    private def getCachedRepoFile(repositoryUrl: URL) = {
        val cachedRepoFileName = md5Hash(repositoryUrl.toString)
        new File(repositoryCacheDir, cachedRepoFileName)
    }
    
    private def md5Hash(s: String) = {
        val digest = MessageDigest.getInstance("MD5")
        digest.update(s.getBytes, 0, s.length)
        new BigInteger(1, digest.digest).toString(16)
    }
    
    def discoverResources(filter: String) = {
        originalAdmin.discoverResources(filter)
    }
    
    def getResource(repositoryId: String) = {
        originalAdmin.getResource(repositoryId)
    }
    
    def listRepositories = {
        surfaceRepositories.values.toArray
    }
    
    def removeRepository(repositoryUrl: URL) = {
        surfaceRepositories -= repositoryUrl
        val cachedRepoFile = getCachedRepoFile(repositoryUrl)
        val conditionOne = originalAdmin.removeRepository(cachedRepoFile.toURI.toURL)
        val conditionTwo = if (cachedRepoFile.exists) {
            cachedRepoFile.delete
            true
        } else {
            false
        }
        saveRepositories
        conditionOne && conditionTwo
    }
    
    def resolver = {
        originalAdmin.resolver
    }
    
    def refreshRepository(repositoryUrl: URL) = {
        removeRepository(repositoryUrl)
        addRepository(repositoryUrl)
    }
    
    def addRepository(repositoryUrl: URL, updateProgress: Option[(Int) => Unit]) = {
        val cachedRepoFile = getCachedRepoFile(repositoryUrl)
        if (!cachedRepoFile.exists) {
            download(from = repositoryUrl, to = cachedRepoFile, updateProgress = updateProgress)
        }
        val cachedRepo = originalAdmin.addRepository(cachedRepoFile.toURI.toURL)
        val surfaceRepo = new Repository {
            def getLastModified = cachedRepo.getLastModified
            def getName = cachedRepo.getName
            def getResources = cachedRepo.getResources
            def getURL = repositoryUrl
            override def hashCode = getURL.hashCode
            override def equals(other: Any) = getURL == other.asInstanceOf[Repository].getURL
        }
        surfaceRepositories += repositoryUrl -> surfaceRepo
        saveRepositories
        surfaceRepo
    }

    private def download(from: URL, to: File, updateProgress: Option[(Int) => Unit]) {
        to.getParentFile.mkdirs
        val bufferSize = 1024
        val connection = from.openConnection
        FileUtil.setProxyAuth(connection)
        connection.connect
        val contentLength = connection.getContentLength
        val is = connection.getInputStream
        val os = new FileOutputStream(to)
        try {
            var eof = false
            var totalNumWrittenBytes = 0
            var previousProgress = -1
            while(!eof) {
                val buffer = new Array[Byte](bufferSize)
                val numReadBytes = is.read(buffer, 0, bufferSize)
                if (numReadBytes > 0) {
                    os.write(buffer, 0, numReadBytes)
                    totalNumWrittenBytes += numReadBytes
                    if (contentLength > 0) {
                        val progress = (100 * totalNumWrittenBytes) / contentLength
                        if (progress != previousProgress) {
                            updateProgress.foreach(_(progress))
                            previousProgress = progress
                        }
                    }
                } else {
                    eof = true
                }
                
            }
        } catch {
            case ex: Throwable =>
                to.delete
                throw ex
        } finally {
            os.close
            is.close
        }
    }
    
    
    private def saveRepositories {
        /* Save repositories to OSGi preferences */
        bundleContext findService withInterface[PreferencesService] andApply { prefService =>
            log.debug("Found PreferencesService")
            log.debug("Get preferences node, remove all previous entries and add repository URLs to it")
            val pref = prefService.getSystemPreferences.node(PathToRepositoriesNode)
            for (key <- pref.keys) {
                pref.remove(key)
            }
            for (repository <- listRepositories) {
                pref.put(repository.getURL.toString, "")
            }
        }
    }
    
    private def restoreRepositories {
        /* Read repositories from OSGi preferences */
        bundleContext findService withInterface[PreferencesService] andApply { prefService =>
            log.debug("Found PreferencesService")
            val sysPref = prefService.getSystemPreferences
            if (sysPref.nodeExists(PathToRepositoriesNode)) {
                log.debug("Found preferences node [" + PathToRepositoriesNode + "]. Add repositories.")
                val pref = sysPref.node(PathToRepositoriesNode)
                for (repositoryUrlString <- pref.keys) {
                    val repositoryUrl = new URL(repositoryUrlString)
                    addRepository(repositoryUrl)
                }
            }
        } match {
            case None =>
                log.debug("Couldn't find PreferencesService")
            case _ =>
        }
    }
    
	def start(context: BundleContext) {
        bundleContext = context
        val logger = new Logger(context)
        adminImpl = new RepositoryAdminImpl(context, logger)
        originalAdmin = Wrapper.wrap(adminImpl)
        context createService (this, interface1 = interface[RepositoryAdmin], interface2 = interface[CachingRepositoryAdmin], properties = Constants.SERVICE_RANKING -> ServiceRanking)
        restoreRepositories
	}
        
	def stop(context: BundleContext) {
        saveRepositories
        surfaceRepositories.clear
        adminImpl.dispose
        adminImpl = null
        originalAdmin = null
        bundleContext = null
	}

}